<?php
/**
 *
 */

/**
 * Una cadena de texto
 */
class LiteString implements ArrayAccess {
	
	/**
	 * Retorna verdadero si, y solo si, <code>$var</code> es una instancia de LiteString
	 * o es de tipo string 
	 * 
	 * @param mixed $var
	 * @return boolean
	 */
	static function isString($var) {
		return $var instanceof self || is_string($var);
	}
	
	/**
	 * @param mixed $var
	 * @return LiteString
	 */
	static function toString($var) {
		return $var instanceof self ? $var : new self( $var );
	}
	
	/**
	 * Representacion interna
	 * @var string
	 */
	private $value;
	
	/**
	 * Longitud de la cadena
	 * @var integer
	 */
	private $length;
	
	/**
	 * 
	 * @param string $value
	 */
	function __construct($value=null) {
		$this->value = (string) $value;
	}

	/**
	 * @return string
	 */
	function __toString() {
		return $this->value;
	}
	
	/**
	 * @param int $offset
	 */
	function offsetExists($offset){
		return isset( $this->value[ $offset ]);
	}

	/**
	 * @param int $offset
	 */
	function offsetGet($offset){
		if( $this->offsetExists($offset)) {
			return $this->value[ $offset ]; 
		}
	}

	/**
	 * @param int $offset
	 * @param int $value
	 */
	function offsetSet($offset, $value){
		throw new RuntimeException('LiteString are inmutables');
	}

	/**
	 * @param int $offset
	 */
	function offsetUnset($offset){
		throw new RuntimeException('LiteString are inmutables');
	}
	
	/**
	 * Retorna verdadero si, y solo si, la cadena es igual a '' o nula
	 * @return boolean
	 */
	function isEmpty() {
		return empty( $this->value );
	}
	
	/**
	 * Retorna verdadero si, y solo si, la cadena no es igual a '' o nula
	 * @return boolean
	 */
	function notEmpty() {
		return !$this->isEmpty();
	}
	
	/**
	 * Retorna < 0 si la cadena es menor que <code>$string</code>, > 0 si la cadena es mayor a <code>$string</code>, y 0 si son iguales 
	 * @param $string
	 * @return int
	 */
	function compare($string) {
		return strcmp($this->value, $string);
	}
	
	/**
	 * Retorna verdadero, si y solo si, la cadena es igual a <code>$string</code>
	 * 
	 * @param string $string
	 * @return boolean
	 */
	function equals($string) {
		return 0 === $this->compare($string);
	}
	
	/**
	 * Retorna verdadero, si y solo si, la cadena es igual a <code>$string</code>
	 * La comparacion no es sensible a mayusculas y minusculas
	 * <code>
	 * 		$str1 = new LiteString('Test');
	 * 		$str2 = new LiteString('tEsT'); 
	 * 		var_dump( $str1->equals($str2)); // false
	 * 		var_dump( $str1->equalsNoCase($str2)); // true
	 * </code>
	 * 
	 * @param string $string
	 */
	function equalsNoCase($string) {
		$string = self::toString($string);
		
		$lowCased = $this->toLowerCase();
		return $lowCased->equals($string->toLowerCase());
	}
	
	/**
	 * Devuelve el caracter de la posicion <code>$index</code>
	 * 
	 * @param $indes
	 * @return string
	 */
	function charAt($index) {
		if( $this->offsetExists($index)) {
			return $this->offsetGet($index);
		}
	}
	
	/**
	 * Longitud de la cadena
	 * 
	 * @return int
	 */
	function length() {
		iF( null === $this->length ) {
			$this->length = strlen( $this->value );
		}
		return $this->length;
	}
	
	/**
	 * Devuelve la posicion de la primera aparicion de <code>$find</code>
	 *
	 * @param string $find
	 * @param int $startFrom
	 * @return int 
	 */
	function indexOf($find, $startFrom = 0){
		return $this->value ? strpos($this->value, $find, $startFrom) : false;
	}
	
	/**
	 * Devuelve la posicion de la ultima aparicion de <code>$find</code>
	 * 
	 * @param string $find
	 * @param int $startFrom
	 * @return int
	 */
	function lastIndexOf($find, $startFrom = 0){
		return $this->value ? strrpos($this->value, $find, $startFrom) : false;
	}
	

	/**
	 * Comprueba si la cadena empieza con <code>$preffix</code>
	 * 
	 * @param string $preffix
	 * @return boolean
	 */
	function startsWith($preffix){
		if( $this->value === $preffix) {
			return true;
		}
		return 0 === $this->indexOf($preffix);
	}
	
	/**
	 * Comprueba si la cadena termina con <code>$suffix</code>
	 * 
	 * @param string $suffix
	 * @return boolean
	 */
	function endsWith($suffix){
		if( $this->equals($suffix)) {
			return true;
		}
		if(false === ($index = $this->lastIndexOf($suffix))) {
			return false;
		}
		return $index === $this->length() - strlen($suffix);
	}
	
	/**
	 * Comprueba si la cadena <code>$string </code>contiene a la cadena <code>$piece</code>
	 * 
	 * @param string $piece
	 * @return boolean
	 */
	function contains($piece){
		return false !== $this->indexOf($piece);
	}
	
	/**
	 * Comprueba si alguna de las <code>$pieces</code> esta contenida en la cadena
	 * 
	 * @param array $pieces
	 * @return boolean
	 */
	function containsAny(array $pieces){
		foreach($pieces as $piece) {
			if($this->contains($piece)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Comprueba que todas las <code>$pieces</code> pertenezcan a la cadena
	 * 
	 * @param array $pieces
	 * @return boolean
	 */
	function containsAll(array $pieces){
		foreach($pieces as $piece) {
			if($this->contains($piece)) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Retorna una porcion de la cadena, desde <code>$start</code> hasta <code>$length</code> caracteres
	 * 
	 * @param int $start
	 * @param int $length
	 * @return LiteString
	 */
	function subString($start, $length = false){
		if( false !== $length ) {
			return new self(substr($this->value, $start, $length));
		}
		return new self(substr($this->value, $start)); 
	}
	
	/**
	 * 
	 * @param string $piece
	 * @param int $startFrom
	 * @return LiteString
	 */
	function subStringBefore($piece, $startFrom=0) {
		if( false !== ( $index = $this->indexOf($piece, $startFrom))) {
			return $this->subString(0, $index);
		}
		return new self($this);
	}
	
	/**
	 * 
	 * @param string $string
	 * @param string $piece
	 * @param int $startFrom
	 */
	function subStringAfter($piece, $startFrom=0) {
		if( false !== ( $index = $this->indexOf($piece, $startFrom))) {
			return $this->subString($index+strlen($piece));
		}
		return new self($this);
	}
	
	
	/**
	 * Valida la cadena contra <code>$find</code>
	 * $find puede ser una {@see RegExp} o un string valido
	 * 
	 * @param string$find
	 * @param int $startFrom
	 * 
	 * @return boolean
	 */
	function matches($find, $startFrom = 0){
		if($startFrom > 0) {
			$piece = $this->subString($this->value, $startFrom);
			return $piece->matches($find);
		}
		return $this->contains($find);
	}
	
	/**
	 * Comprueba si la cadena valida al menos una expresion  
	 * 
	 * @param array $finds Vector de string o RegExp
	 * @param int $startFrom
	 * 
	 * @return boolean
	 */
	function matchAny(array $finds, $startFrom = 0){
		foreach($finds as $find) {
			if( $this->matches($find, $startFrom)) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Comprueba si la cadena valida todas las expresiones
	 * 
	 * @param array $finds Vector de string o RegExp
	 * @param int $startFrom
	 * 
	 * @return boolean
	 */
	function matchAll(array $finds, $startFrom = 0){
		foreach($finds as $find) {
			if(!$this->matches($find, $startFrom)) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * @param string $find
	 * @param string $replacement
	 * 
	 * @return string
	 */
	function replace($find, $replacement){
		return $this->replaceFirst($find, $replacement);
	}

	/**
	 * 
	 * @param string $find
	 * @param string $replacement
	 * @param int $count
	 * 
	 * @return LiteString
	 */
	function replaceFirst($find, $replacement, $count = 1){
		return new self( str_replace($find, $replacement, $this->value, $count));
	}
	
	/**
	 * @param string $find
	 *
	 * @return LiteString
	 */
	function remove($find){
		return $this->replace($find, '');
	}
	
	/**
	 * @param string $delimiter
	 * @param int $limit
	 * @return array
	 */
	function explode($delimiter, $limit=null) {
		if( !$delimiter ) {
			return array( new self($this) );
		}
		$ret = $limit !== null ? 
			explode($delimiter, $this->value, $limit) : 
			explode($delimiter, $this->value);
		
		return false === $ret ? array( new self($this) ) : $ret;
	}
	
	/**
	 * Retorna una copia de la cadena con todas sus letras en minusculas
	 * 
	 * @return LiteString
	 */
	function toLowerCase() {
		return new self( strtolower( $this->value ));
	}
	
	/**
	 * Retorna una copia de la cadena con todas sus letras en mayusculas
	 * 
	 * @return LiteString
	 */
	function toUpperCase() {
		return new self( strtoupper( $this->value ));
	}
	
	/**
	 * 
	 * @param string $string
	 */
	function upperCaseWords() {
		return new self( ucwords( $this->value ));
	}
}